/*
* Copyright (C) Microsoft. All rights reserved.  
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not 
* use this file except in compliance with the License.  You may obtain a copy 
* of the License at http://www.apache.org/licenses/LICENSE-2.0  
* 
* THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 
* WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 
* MERCHANTABLITY OR NON-INFRINGEMENT. 
*
* See the Apache License, Version 2.0 for specific language governing 
* permissions and limitations under the License.
*
*/
#include "pch.h"
#include <baja/stream/storage_file.h>
#include <baja/transition_tracking/transition_tracking_helpers.h>
#include <journal/models/photo_model.h>
#include <journal/models/photo_model_transitions.h>

using namespace journal::model;

namespace bt = baja::transition_tracking;

b::uint32 photo_model::_maxMediaFileId = 0;
const b::uint32 photo_model::_photosReadyPass = 1;

photo_model::photo_model(
    const b::uint32& typeToken,
    const std::shared_ptr<ipage_model>& parent, 
    std::shared_ptr<journal::model::db::iphoto_table> photoTable,
    std::shared_ptr<journal::model::db::imedia_file_table> mediaFileTable,
    b::uint32 photoId
    ) : 
        _initialized(false),
        _inDB(false),
        _disposed(false),
        _active(true),
        _readyEvent(nullptr),
        _angle(0.0f),
        _position(0.0f, 0.0f),
        _scale(1.0f),
        _parent(parent), 
        _photoTable(photoTable),
        _mediaFileTable(mediaFileTable),
        _mediaFileId(0)
{
    // generic_model
    setGenericIdentity(photoId);
    setGenericTypeToken(typeToken);
    setGenericParent(std::dynamic_pointer_cast<bm::igeneric_model>(parent));
    // tracked_object
    setTrackingService(constants::undoRedoService());

    constants::memoryTrackingService()->logCreate(genericTypeToken(), genericIdentity(), sizeof(photo_model));
}

photo_model::~photo_model()
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"XX photo_model::~photo_model() : genericIdentity() = %i") %
        genericIdentity()
        );

    constants::memoryTrackingService()->logDispose(genericTypeToken(), genericIdentity(), sizeof(photo_model));
}

std::shared_ptr<photo_model> photo_model::create(
    const b::uint32& typeToken,
    const std::shared_ptr<ipage_model>& parent, 
    const std::shared_ptr<journal::model::db::iphoto_table>& photoTable,
    const std::shared_ptr<journal::model::db::imedia_file_table>& mediaFileTable,
    const b::uint32& photoId)
{
    std::shared_ptr<photo_model> returnValue = std::make_shared<photo_model>(
        typeToken, 
        parent, 
        photoTable,
        mediaFileTable,
        photoId);

    returnValue->_readyEvent = ::CreateEventEx(nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS);
    return returnValue;
}

b::Boolean photo_model::isAccessible()
{
    return _initialized && !_disposed;
}

void photo_model::setLocalFile(
    const std::shared_ptr<b::storage::istorage_file>& localStorageFile,
    const std::shared_ptr<b::imemory_stream>& localStorageFileStream)
{
    _localStorageFile = localStorageFile;
    _localStorageFileStream = localStorageFileStream;
    SetEvent(_readyEvent);
}

void photo_model::setAllAttributes(
    const b::uint32& mediaFileId,
    const std::shared_ptr<baja::storage::istorage_file>& localStorageFile,
    const std::shared_ptr<b::imemory_stream>& localStorageFileStream,
    const b::float32& scale,
    const m::point2d& position,
    const b::float32& angle)
{
    if ( (_scale != scale) || (_position != position) || (_angle != angle) || (_localStorageFile != localStorageFile) || (_localStorageFileStream != localStorageFileStream))
    {
        // register the transition...
        registerUpdate(
            _active,
            localStorageFile,
            localStorageFileStream,
            scale,
            position,
            angle);

        // update state
        _mediaFileId = mediaFileId;

        setLocalFile(localStorageFile, localStorageFileStream);

        _scale = scale;
        _position = position;
        _angle = angle;

        fireChangedEvent();
    }
}

void photo_model::initializeAsync(
    const std::shared_ptr<baja::storage::istorage_file>& originalStorageFile,
    const b::float32& scale,
    const m::point2d& position,
    const b::float32& angle,
    const std::function<void()>& initializeCompleted)
{
    ensureLocalFileAsync(
        originalStorageFile,
        [this, scale, position, angle, initializeCompleted](std::shared_ptr<b::storage::istorage_file> localStorageFile, std::shared_ptr<b::imemory_stream> localStorageFileStream)
        {
            this->setAllAttributes(_mediaFileId, localStorageFile, localStorageFileStream, scale, position, angle);
            this->markReady();
            initializeCompleted();
        }
    );
}

std::shared_ptr<ipage_model> photo_model::parent()
{
    return _parent.lock();
}

std::wstring photo_model::textureFile()
{
    WaitForSingleObjectEx(_readyEvent, INFINITE, FALSE);

    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return L"";
    }

    return _localStorageFile->fullFileName();
}

std::shared_ptr<b::imemory_stream> photo_model::textureFileStream()
{
    WaitForSingleObjectEx(_readyEvent, INFINITE, FALSE);

    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return nullptr;
    }

    return _localStorageFileStream;
}

const b::float32 photo_model::scale()
{
    return _scale;
}

const b::math::point2d& photo_model::position()
{
    return _position;
}

const b::float32 photo_model::angle()
{
    return _angle;
}

void photo_model::firePhotoMovedEvent()
{
    _observerSource.raise(
        [&, this](const std::shared_ptr<iphoto_model_observer>& observer, void* context)
        {
            observer->onPhotoMoved(shared_from_this(), this->_scale, this->_position, this->_angle);
        });

    saveUpdatedSelf();
}

void photo_model::setSpatialAttributes(
    const b::float32& scale,
    const m::point2d& position,
    const b::float32& angle
    )
{
    {
        BAJA_LOCK(_mutex);
        if (!isAccessible())
        {
            return;
        }

        setAllAttributes(_mediaFileId, _localStorageFile, _localStorageFileStream, scale, position, angle);
    }

    firePhotoMovedEvent();
}

b::int32 photo_model::addObserver(const std::shared_ptr<iphoto_model_observer>& observer)
{
    return _observerSource += observer;
}

void photo_model::removeObserver(const std::shared_ptr<iphoto_model_observer>& observer)
{
    _observerSource -= observer;
}

void photo_model::removeObserver(const b::int32 cookie)
{
    _observerSource -= cookie;
}

b::uint32 photo_model::storageId()
{
    return genericIdentity();
}

bool photo_model::isFileLocal(std::wstring fullFileName)
{
    std::wstring::size_type l = fullFileName.rfind(L'\\');
    std::wstring pathName;

    if (l != std::wstring::npos)
    {
        pathName = fullFileName.substr(0, l);
    }
    return 0 == _wcsicmp(pathName.c_str(), Windows::Storage::ApplicationData::Current->LocalFolder->Path->Data());
}

std::wstring photo_model::stampFileName(
    const std::wstring& fileName,
    const long long& value)
{
    // 64bit = 8 bytes = 16 characters + \0 = 17 characters.
    // 0x0123456701234567
    b::wchar buffer[17] = {0};
    _i64tow_s(value, buffer, sizeof(buffer)/sizeof(buffer[0]), 16);
    std::wstring stamp(L"."); stamp += buffer;

    std::wstring stampedFileName = fileName;

    std::wstring::size_type l = stampedFileName.rfind(L'.');
    if (l != std::wstring::npos)
    {
        stampedFileName = stampedFileName.insert(l, stamp);
    }
    else
    {
        stampedFileName += buffer;
    }

    return stampedFileName;
}

void photo_model::initializeMaxMediaFileId()
{
    BAJA_LOCK(_mutex);

    static bool maxMediaFileIdInitialized = false;
    if (maxMediaFileIdInitialized == false)
    {
        _maxMediaFileId = _mediaFileTable->common()->getNextId();
        maxMediaFileIdInitialized = true;
    }
}

void photo_model::acquireNewMediaFileId()
{
    BAJA_LOCK(_mutex);

    initializeMaxMediaFileId();

    _mediaFileId = InterlockedIncrement(&_maxMediaFileId);
}

void photo_model::ensureLocalFileAsync(
    const std::shared_ptr<b::storage::istorage_file>& originalStorageFile,
    const std::function<void(std::shared_ptr<b::storage::istorage_file> localStorageFile,
                             std::shared_ptr<b::imemory_stream> localStorageFileStream)>& ensureCompleted)
{
    b::uint32 mediaFileId;
    std::wstring localMediaFileName;
    std::wstring stampedOriginalFileName = stampFileName( originalStorageFile->fullFileName(), originalStorageFile->modifiedDateTime());

    std::shared_ptr<photo_model> p_this = std::static_pointer_cast<photo_model>(shared_from_this());

    if (_mediaFileTable->findMediaFile(stampedOriginalFileName, mediaFileId, localMediaFileName) == false)
    {
        acquireNewMediaFileId();

        if (isFileLocal(stampedOriginalFileName))
        {
            // for local files we do not stamp them since they will be loaded from the local folder (which happens to be the original folder too).
            insertMediaFileRecord(originalStorageFile->fullFileName(), originalStorageFile->fullFileName());
            originalStorageFile->createMemoryStreamFromFileAsync(
                    [ensureCompleted, originalStorageFile](bool success, const std::shared_ptr<b::imemory_stream>& localStorageFileStream)
                    {
                        if (!success)
                        {
                            BAJA_THROW(std::logic_error("an error occured while loading file content"));
                        }
                        ensureCompleted(originalStorageFile, localStorageFileStream);
                    }
                );
        }
        else
        {
            // we need to copy the file...
            std::wstring localFullFileName = generateLocalFullFileName(generateLocalFileName(stampedOriginalFileName));

            BAJA_ASSERT(originalStorageFile != nullptr, L"original storage file is null");
            // we have a file handle, use it...
            originalStorageFile->copyToAsync(localFullFileName,
                    [p_this, stampedOriginalFileName, localFullFileName, ensureCompleted](bool success, std::shared_ptr<b::storage::istorage_file> copiedStorageFile)
                    {
                        if (!success)
                        {
                            BAJA_THROW(std::logic_error("an error occured while executing copyToAsync()"));
                        }

                        p_this->insertMediaFileRecord(stampedOriginalFileName, localFullFileName);
                        copiedStorageFile->createMemoryStreamFromFileAsync(
                                [ensureCompleted, copiedStorageFile](bool success, const std::shared_ptr<b::imemory_stream>& localStorageFileStream)
                                {
                                    if (!success)
                                    {
                                        BAJA_THROW(std::logic_error("an error occured while loading file content"));
                                    }
                                    ensureCompleted(copiedStorageFile, localStorageFileStream);
                                }
                        );
                    }
                );
        }
    }
    else
    {
        // we do not need to copy the file...
        _mediaFileId = mediaFileId;

        std::shared_ptr<b::storage::istorage_file> copiedStorageFile = s::createStorageFile();
        copiedStorageFile->initializeAsync(localMediaFileName,
                [p_this, ensureCompleted, copiedStorageFile](bool success)
                {
                    if (!success)
                    {
                        BAJA_THROW(std::logic_error("an error occured while executing initializeAsync()"));
                    }
                    copiedStorageFile->createMemoryStreamFromFileAsync(
                            [ensureCompleted, copiedStorageFile](bool success, const std::shared_ptr<b::imemory_stream>& localStorageFileStream)
                            {
                                if (!success)
                                {
                                    BAJA_THROW(std::logic_error("an error occured while loading file content"));
                                }
                                ensureCompleted(copiedStorageFile, localStorageFileStream);
                            }
                    );
                }
            );
    }
}

void photo_model::saveUpdatedSelf()
{
    if (_inDB == false)
    {
        return;
    }

    DEBUG_MODEL_DUMP(this, false, L"<< photo_model::saveUpdatedSelf()");

    _photoTable->update(genericIdentity(),
        _parent.lock()->storageId(),
        true,
        _mediaFileId,
        _scale,
        _angle,
        _position.x,
        _position.y);
}

void photo_model::createInDB()
{
    b::stopwatch ws;

    BAJA_LOCK(_mutex);

    if (_inDB == true)
    {
        return;
    }

    DEBUG_MODEL_DUMP(this, false, L"<< photo_model::saveSelf() : saving");

    _photoTable->insert(
        genericIdentity(),
        _parent.lock()->storageId(),
        true,
        _mediaFileId,
        _scale,
        _angle,
        _position.x,
        _position.y);

    _inDB = true;

    constants::performanceService()->collect(performance_event::modelSave, ws.getElapsedMilliseconds()); 
}

void photo_model::initializeLocalFileAsync(std::wstring localFileName, const std::function<void()>& initializeCompleted)
{
    // make sure it's only the file name without a path...
    std::wstring::size_type l = localFileName.rfind(L'\\');
    if (l != std::wstring::npos)
    {
        localFileName = localFileName.substr(l + 1);
    }

    std::shared_ptr<photo_model> p_this = std::static_pointer_cast<photo_model>(shared_from_this());

    std::shared_ptr<b::storage::istorage_file> localStorageFile = s::createStorageFile();
    localStorageFile->initializeAsync(generateLocalFullFileName(localFileName),
            [p_this, localStorageFile, initializeCompleted](bool success)
            {
                if (!success)
                {
                    BAJA_THROW(std::logic_error("an error occured while executing initializeAsync()"));
                }
                localStorageFile->createMemoryStreamFromFileAsync(
                        [p_this, localStorageFile, initializeCompleted](bool success, const std::shared_ptr<b::imemory_stream>& localStorageFileStream)
                        {
                            if (!success)
                            {
                                BAJA_THROW(std::logic_error("an error occured while executing createMemoryStreamFromFileAsync()"));
                            }
                            p_this->setLocalFile(localStorageFile, localStorageFileStream);
                            initializeCompleted();
                        }
                    );
            }
        );
}

void photo_model::loadSelf()
{
    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L">>                     self  : genericIdentity() = %i") %
        genericIdentity()
        );

    _photoTable->load(
        genericIdentity(),
        _parent.lock()->storageId(),
        _active,
        _mediaFileId,
        _scale,
        _angle,
        _position.x,
        _position.y);

    std::wstring originalFileName;

    _mediaFileTable->load(
        _mediaFileId,
        _localFileName,
        originalFileName);

    _inDB = true;

    DEBUG_MODEL_DUMP(this, false, L">> photo_model loaded       ");
}

void photo_model::loadAsync(std::function<void()> loadCompleted)
{
    b::stopwatch ws;

    BAJA_LOCK(_mutex);

    std::shared_ptr<photo_model> p_this = std::static_pointer_cast<photo_model>(shared_from_this());

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL, L">> photo_model loading");

    if (_initialized == false)
    {
        loadSelf();
        initializeLocalFileAsync(_localFileName, 
            [p_this, &ws, loadCompleted]()
            {
                BAJA_LOCK(p_this->_mutex);

                p_this->markReady();
                constants::performanceService()->collect(performance_event::modelLoad, ws.getElapsedMilliseconds()); 
                loadCompleted();
            }
        );
    }
    else
    {
        // We have to signal the 'markReady' on another thread to avoid blocking.
        // Also, it has to block on the ready event because the previous pass might not have finished
        // by the time the second pass starts.
        concurrency::task<void> t(
                [p_this, &ws, loadCompleted]()
                {
                    BAJA_LOCK(p_this->_mutex);

                    WaitForSingleObjectEx(p_this->_readyEvent, INFINITE, FALSE);
                    p_this->markReady();
                    constants::performanceService()->collect(performance_event::modelLoad, ws.getElapsedMilliseconds()); 
                    loadCompleted();
                }
        );
    }
}

void photo_model::unload()
{
    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return;
    }

    // no children to unload/uncreate.
}

void photo_model::activate(
    const b::Boolean& active)
{
    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return;
    }

    // by defintion, if you are activating or deactivating a record, it means there is a record already.
    _inDB = true;

    _photoTable->update(genericIdentity(),
        _parent.lock()->storageId(),
        active,
        _mediaFileId,
        _scale,
        _angle,
        _position.x,
        _position.y);

    registerCustomEvent(
        photo_model_transition_type::notReadyEventTransition
        );

    registerUpdate(
        active,
        _localStorageFile,
        _localStorageFileStream,
        _scale,
        _position,
        _angle);
}

void photo_model::dispose()
{
    BAJA_LOCK(_mutex);
    if (!isAccessible())
    {
        return;
    }
    if (_inDB == false)
    {
        return;
    }
    removeFromDB(_photoTable, _parent.lock()->storageId());
    _inDB = false;
    _initialized = false;
}

#ifdef DEBUG_ENABLE_MODELS
void photo_model::dump(bool selfOnly, std::wstring prefix)
{
    BAJA_LOCK(_mutex);

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"%s : genericIdentity() = %i,\t\t_pageId = %i,\t_initialized = %s,\t_inDB = %s") %
        prefix.c_str() %
        genericIdentity() %
        _parent.lock()->storageId() %
        (_initialized ? L"true" : L"false") %
        (_inDB ? L"true" : L"false")
        );

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"%s :                   \t_mediaFileId = %i") %
        prefix.c_str() %
        _mediaFileId
        );

    DEBUG_CONSOLE_TRACE_TAG(TRACE_TAG_MODEL,
        boost::wformat(L"%s :                   \t_scale = %0.2f, _angle = %0.2f, _position.x = %0.2f, _position.y = %0.2f") %
        prefix.c_str() %
        _scale %
        _angle %
        _position.x %
        _position.y
        );
}
#endif

std::wstring photo_model::generateLocalFileName(std::wstring originalFileName)
{
    std::wstring fileName;
    std::wstring::size_type l = originalFileName.rfind(L'\\');
    if (l == std::wstring::npos)
    {
        fileName = originalFileName;
    }
    else
    {
        fileName = originalFileName.substr(l + 1);
    }

    std::wostringstream outs;
    outs << _mediaFileId;
    outs << L"_";
    outs << fileName;

    return std::wstring(outs.str());
}

std::wstring photo_model::generateLocalFullFileName(std::wstring fileName)
{
    std::wstring targetFolder(Windows::Storage::ApplicationData::Current->LocalFolder->Path->Data());

    std::wostringstream outs;
    outs << targetFolder;
    outs << L"\\";
    outs << fileName;

    return std::wstring(outs.str());
}

void photo_model::insertMediaFileRecord(std::wstring originalFullFileName, std::wstring localFullFileName)
{
    _mediaFileTable->insert(_mediaFileId, localFullFileName, originalFullFileName);
}

void photo_model::markReady()
{
    _initialized = true;

    registerCustomEvent(
        photo_model_transition_type::readyEventTransition
        );
}

void photo_model::removeFromDB(
    const std::shared_ptr<journal::model::db::iphoto_table>& photoTable,
    const b::uint32 pageId)
{
    photoTable->dispose(pageId);
}


std::shared_ptr<iphoto_model> journal::model::createPhoto(
    const b::uint32& typeToken,
    const std::shared_ptr<ipage_model>& parent, 
    const std::shared_ptr<journal::model::db::iphoto_table>& photoTable,
    const std::shared_ptr<journal::model::db::imedia_file_table>& mediaFileTable,
    const b::uint32& photoId)
{
    std::shared_ptr<photo_model> returnValue = photo_model::create(
        typeToken,
        parent, 
        photoTable,
        mediaFileTable,
        photoId);
    return std::dynamic_pointer_cast<iphoto_model>(returnValue);
}

void journal::model::removePhotos(
    const std::shared_ptr<journal::model::db::iphoto_table>& photoTable,
    const b::uint32& pageId)
{
    photo_model::removeFromDB(photoTable, pageId);
}